﻿using SharpDepend.Datatypes;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;

namespace SharpDepend.WPF
{
    public class MultiTouchManager : IMultiTouchManager
    {
        public IMultiTouch CreateMultiTouch<T>(T onUIElement)
        {
            return new MultiTouch((UIElement)(object)onUIElement);
        }
    }

    /// <summary>
    /// Reference: http://www.codeproject.com/Articles/692286/WPF-and-multi-touch
    /// </summary>
    public class MultiTouch : IMultiTouch
    {
        /// <summary>
        /// Class to make an WPF UIElement to be movable in a Canvas.
        /// <para>Use the mouse to move it.</para>
        /// </summary>
        private class MovableTrackObject
        {
            private UIElement mParentElement;
            private UIElement mElement;
            private bool mMoving;
            private Point mLastMousePoint;
            private Point mDeltaPoint;

            /// <summary>
            /// Occurs when mouse wheel is scrolling while the pointer is over this element.
            /// </summary>
            public event Action<Point, MouseWheelEventArgs> OnMouseWheel;

            /// <summary>
            /// Occurs when any mouse button is pressed while the pointer is over this element.
            /// </summary>
            public event Action<Point, MouseEventArgs> OnMouseDown;

            /// <summary>
            /// Calls when object has moved. Moving width delta position.
            /// <para>Arg1 = delta position, Arg2 = current position.</para>
            /// </summary>
            public event Action<Point, Point, MouseEventArgs> OnMouseMove;

            /// <summary>
            /// Occurs when any mouse button is released over this element.
            /// <para>Arg1 = delta position, Arg2 = current position.</para>
            /// </summary>
            public event Action<Point, Point, MouseEventArgs> OnMouseUp;

            /// <summary>
            /// Create instance of MovableTrackObject.
            /// </summary>
            /// <param name="parentElement">Parent element of the moveable element.</param>
            /// <param name="element">Element to be movable.</param>
            public MovableTrackObject(UIElement parentElement, UIElement element)
            {
                mParentElement = parentElement;
                mElement = element;

                mElement.PreviewMouseDown += ZReference_MouseDown;
                mElement.PreviewMouseMove += ZReference_MouseMove;
                mElement.PreviewMouseUp += ZReference_MouseUp;
                mElement.MouseWheel += mElement_MouseWheel;
            }

            void mElement_MouseWheel(object sender, MouseWheelEventArgs e)
            {
                if (OnMouseWheel != null)
                {
                    //Point point = e.GetPosition(mParentElement);
                    Point point = mLastMousePoint; // Note: OpenGL uses WindowsFormHost and has problems with mouse wheel.
                    OnMouseWheel(point, e);
                }
            }

            /// <summary>
            /// Calls when mouse down.
            /// </summary>
            void ZReference_MouseDown(object sender, MouseButtonEventArgs e)
            {
                // Only mouse.
                if (e.StylusDevice == null)
                {
                    if (Mouse.Captured == mElement || Mouse.Captured == null || true)
                    {
                        if (!Mouse.Capture(mElement, CaptureMode.SubTree))
                        {
                            Log.Warning(this, "A UIElement (" + mElement.GetType().Name + ") was not able to be movable.");
                        }
                        else
                        {
                            if (mMoving = mElement.CaptureMouse())
                            {
                                mLastMousePoint = e.GetPosition(mParentElement);
                                mDeltaPoint.X = 0;
                                mDeltaPoint.Y = 0;

                                if (OnMouseDown != null)
                                {
                                    OnMouseDown(mLastMousePoint, e);
                                }
                            }
                            else
                            {
                                Log.Warning(this, "A UIElement (" + mElement.GetType().Name + ") was not able to move on mouse down."); ;
                            }
                        }
                    }
                }
            }

            /// <summary>
            /// Calls when mouse move.
            /// </summary>
            void ZReference_MouseMove(object sender, MouseEventArgs e)
            {
                // Only mouse.
                if (e.StylusDevice == null)
                {
                    if (Mouse.Captured == mElement || true)
                    {
                        if (mMoving)
                        {
                            Point current = e.GetPosition(mParentElement);
                            Point deltaPoint = current - (Vector)mLastMousePoint;

                            mDeltaPoint += (Vector)deltaPoint;

                            if (OnMouseMove != null)
                            {
                                OnMouseMove(mDeltaPoint, current, e);
                            }

                            // TODO: Fixa så att förflyttningen anpassas efter zoomen :)
                            mLastMousePoint = current;
                        }
                    }
                }
            }

            /// <summary>
            /// Calls when mouse up.
            /// </summary>
            void ZReference_MouseUp(object sender, MouseButtonEventArgs e)
            {
                // Only mouse.
                if (e.StylusDevice == null)
                {
                    if (Mouse.Captured == mElement || true)
                    {
                        mElement.ReleaseMouseCapture();
                        mMoving = false;

                        if (OnMouseUp != null)
                        {
                            Point current = e.GetPosition(mParentElement);
                            OnMouseUp(mDeltaPoint, current, e);
                        }
                    }
                }
            }
        }

        private Dictionary<int, TouchLocation> current = new Dictionary<int, TouchLocation>();
        private Dictionary<int, bool> current_hasBeenStated = new Dictionary<int, bool>();
        private List<int> s_releasedToRemove = new List<int>();
        private UIElement mElement;
        private MovableTrackObject mMoveable;
        private HashSet<int> mMovingTouch;

        private static MultiTouch sFirstInstance;

        public MultiTouch(UIElement element)
        {
            mElement = element;

            mMovingTouch = new HashSet<int>();

            mMoveable = new MovableTrackObject(element, element);
            mMoveable.OnMouseDown += mMoveable_OnMouseDown;
            mMoveable.OnMouseMove += moveable_OnObjectMoved;
            mMoveable.OnMouseUp += mMoveable_OnMouseUp;
            mMoveable.OnMouseWheel += mMoveable_OnMouseWheel;

            mElement.PreviewTouchDown += touch_TouchDown;
            mElement.PreviewTouchMove += touch_TouchMove;
            mElement.PreviewTouchUp += touch_TouchUp;

            // First instance will be stored. If several instances of multitoush is created the
            // it will share GetState method until it reach the first instance again.
            if (sFirstInstance == null)
            {
                sFirstInstance = this;
            }
        }

        private int GetUniqueId()
        {
            bool isUnique = false;
            int id = 0;
            while (!isUnique)
            {
                bool equal = false;
                foreach (var item in current)
                {
                    if (id == item.Value.ID)
                    {
                        id++;
                        equal = true;
                        break;
                    }
                }

                if (!equal)
                {
                    break;
                }
            }

            return id;
        }

        #region Mouse.

        void mMoveable_OnMouseWheel(Point position, MouseWheelEventArgs arg2)
        {
            if (!current.ContainsKey(-2))
            {
                TouchLocation location = new TouchLocation(-2, position.X, position.Y, TouchLocationState.Invalid);
                SetMouseAttributes(arg2, ref location);
                location.MouseWheel = arg2.Delta;

                current.Add(-2, location);
            }
        }

        void mMoveable_OnMouseDown(Point position, MouseEventArgs arg2)
        {
            // ADD.
            if (current.ContainsKey(-1))
            {
                // Yet only happens when you beak the debugger in the same
                // stacktrace as the mouse down stack and continue later.
                current.Remove(-1);
                current_hasBeenStated.Remove(-1);
            }
            TouchLocation location = new TouchLocation(-1, position.X, position.Y, TouchLocationState.Pressed);
            SetMouseAttributes(arg2, ref location);

            current.Add(-1, location);
            current_hasBeenStated.Add(-1, false);
        }

        private static void SetMouseAttributes(MouseEventArgs arg2, ref TouchLocation location)
        {
            location.IsMouse = true;

            if (arg2.LeftButton == MouseButtonState.Pressed)
            {
                location.MouseButton = SharpDepend.Datatypes.MouseButton.Left;
            }
            else if (arg2.MiddleButton == MouseButtonState.Pressed)
            {
                location.MouseButton = SharpDepend.Datatypes.MouseButton.Middle;
            }
            else if (arg2.RightButton == MouseButtonState.Pressed)
            {
                location.MouseButton = SharpDepend.Datatypes.MouseButton.Right;
            }
        }

        void moveable_OnObjectMoved(Point delta, Point position, MouseEventArgs arg3)
        {
            if (current_hasBeenStated[-1])
            {
                TouchLocation location = new TouchLocation(-1, position.X, position.Y, TouchLocationState.Moved);
                SetMouseAttributes(arg3, ref location);
                current[-1] = location;
            }
        }

        void mMoveable_OnMouseUp(Point delta, Point position, MouseEventArgs arg2)
        {
            TouchLocation location = new TouchLocation(-1, position.X, position.Y, TouchLocationState.Released);
            SetMouseAttributes(arg2, ref location);
            current[-1] = location;
        }

        #endregion

        #region Touch.

        void touch_TouchDown(object sender, System.Windows.Input.TouchEventArgs e)
        {
            lock (this)
            {
                Down(e);
            }
        }

        private void Down(System.Windows.Input.TouchEventArgs e)
        {
            Point position = e.GetTouchPoint(mElement).Position;

            if (current.ContainsKey(e.TouchDevice.Id))
            {
                // UPDATE.
                TouchLocation touch = current[e.TouchDevice.Id];
                touch.X = position.X;
                touch.Y = position.Y;
                touch.State = TouchLocationState.Pressed;
                current[e.TouchDevice.Id] = touch;

                if (!current_hasBeenStated.ContainsKey(e.TouchDevice.Id))
                {
                    current_hasBeenStated.Add(e.TouchDevice.Id, false);
                }
                else
                {
                    current_hasBeenStated[e.TouchDevice.Id] = false;
                }

                mMovingTouch.Remove(e.TouchDevice.Id);
            }
            else
            {
                // ADD.
                current.Add(e.TouchDevice.Id, new TouchLocation(GetUniqueId(), position.X, position.Y, TouchLocationState.Pressed));
                current_hasBeenStated.Add(e.TouchDevice.Id, false);
            }
        }

        void touch_TouchMove(object sender, System.Windows.Input.TouchEventArgs e)
        {
            Application.Current.Dispatcher.BeginInvoke((Action)delegate()
            {
                TouchMove(e);
            }, System.Windows.Threading.DispatcherPriority.Send);
        }

        private void TouchMove(System.Windows.Input.TouchEventArgs e)
        {
            Point position = e.GetTouchPoint(mElement).Position;

            if (!current_hasBeenStated.ContainsKey(e.TouchDevice.Id))
            {
                Down(e);
            }

            if (current_hasBeenStated[e.TouchDevice.Id])
            {
                if (!mMovingTouch.Contains(e.TouchDevice.Id))
                {
                    mMovingTouch.Add(e.TouchDevice.Id);

                    TouchLocation touch = current[e.TouchDevice.Id];
                    touch.X = position.X;
                    touch.Y = position.Y;
                    touch.State = TouchLocationState.Moved;
                    current[e.TouchDevice.Id] = touch;

                    mMovingTouch.Remove(e.TouchDevice.Id);
                }
            }
        }

        void touch_TouchUp(object sender, System.Windows.Input.TouchEventArgs e)
        {
            lock (this)
            {
                Point position = e.GetTouchPoint(mElement).Position;

                TouchLocation touch = current[e.TouchDevice.Id];
                touch.X = position.X;
                touch.Y = position.Y;
                touch.State = TouchLocationState.Released;
                current[e.TouchDevice.Id] = touch;
            }
        }

        #endregion
        
        private Dictionary<int, TouchLocation[]> mPreArrays2 = new Dictionary<int, TouchLocation[]>();

        private TouchLocation[] GetList(int count)
        {
            if (mPreArrays2.ContainsKey(count))
            {
                return mPreArrays2[count];
            }
            else
            {
                var touches = new TouchLocation[count];
                mPreArrays2.Add(count, touches);
                return touches;
            }
        }

        public TouchCollection GetState()
        {
            //// Share the same method until the first instance is reached.
            //if (sFirstInstance == this)
            //{
            //    // NOTE: Now, remove mouse down if it is triggered. Otherwise mouse down event will
            //    // fire multiplie times and will cause bugs.
            //    if (current.ContainsKey(-1))
            //    {
            //        if(current[-1].State == TouchLocationState.Pressed)
            //        {
            //            current.Remove(-1);
            //        }
            //    }
            //}

            s_releasedToRemove.Clear();

            TouchLocation[] list = GetList(current.Count);
            int i = -1;
            foreach (var item in current)
            {
                list[++i] = item.Value;

                // If mouse/touch down already, change state to moved.
                if (current_hasBeenStated.ContainsKey(item.Key) && current_hasBeenStated[item.Key] == true &&
                    item.Value.State == TouchLocationState.Pressed)
                {
                    list[i].State = TouchLocationState.Moved;
                }

                current_hasBeenStated[item.Key] = true;

                if (item.Value.State == TouchLocationState.Released)
                {
                    s_releasedToRemove.Add(item.Key);
                }
                else if (item.Value.MouseWheel != 0.0) // Remove mouse wheel.
                {
                    s_releasedToRemove.Add(item.Key);
                }
            }

            // Remove released touches.
            for (i = s_releasedToRemove.Count - 1; i >= 0; i--)
            {
                current.Remove(s_releasedToRemove[i]);
                current_hasBeenStated.Remove(s_releasedToRemove[i]);
            }
            
            return new TouchCollection(list);
        }
    }
}
